/*
 *  FastCGI Process Manager for Windows
 *
 */

#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0500
#endif //_WIN32_WINNT

#include <windows.h>
#include <winsock.h>
#include <wininet.h>
#include <stdbool.h>
#define SHUT_RDWR SD_BOTH

#ifndef JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
#define JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (0x2000)
#endif

HANDLE FcpJobObject;

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <string.h>
#include <errno.h>

#include <stdarg.h>
#include <time.h>

#define MAX_PROCESSES 1024
#define FILE_MAX_SIZE (1024 * 1024)
static const char version[] = "Revision: 2025.09.27";
static char *prog_name;
int number = 1;
int port = 9000;
char *ip = "127.0.0.1";
char *user = "";
char *root = "";
char *path = "";
char *group = "";
int listen_fd;
struct sockaddr_in listen_addr;
int process_fp[MAX_PROCESSES];
int process_idx = 0;
HANDLE threads[MAX_PROCESSES];

static struct option longopts[] = 
    {
        {"help", no_argument, NULL, 'h'},
        {"version", no_argument, NULL, 'v'},
        {"number", required_argument, NULL, 'n'},
        {"ip", required_argument, NULL, 'i'},
        {"port", required_argument, NULL, 'p'},
        {NULL, 0, NULL, 0}
    };

static char opts[] = "hvnip";

static void usage(FILE *where)
{
    fprintf(where, ""
                   "Usage: %s path [-n number] [-i ip] [-p port]\n"
                   "Manage FastCGI processes.\n"
                   "\n"
                   " -n, --number  number of processes to keep\n"
                   " -i, --ip      ip address to bind\n"
                   " -p, --port    port to bind, default is 9000\n"
                   " -h, --help    output usage information and exit\n"
                   " -v, --version output version information and exit\n"
                   "",
            prog_name);
    exit(where == stderr ? 1 : 0);
}

static void print_version()
{
    printf("\n %s %s\n FastCGI Process Manager\n Compiled on %s\n", prog_name, version, __DATE__);
    exit(0);
}

long get_file_size(char *filename)
{
    long length = 0;
    FILE *fp = NULL;
    fp = fopen(filename, "rb");
    if (fp != NULL)
    {
        fseek(fp, 0, SEEK_END);
        length = ftell(fp);
    }
    if (fp != NULL)
    {
        fclose(fp);
        fp = NULL;
    }
    return length;
}

int write_log(char *filename, const char *format, ...)
{
    if (filename != NULL && format != NULL)
    {
        long length = get_file_size(filename);
        if (length > FILE_MAX_SIZE)
        {
            unlink(filename);
        }

        FILE *pFile = fopen(filename, "a+b");
        if (pFile != NULL)
        {
            va_list arg;
            int done;

            va_start(arg, format);

            time_t time_log = time(NULL);
            struct tm *tm_log = localtime(&time_log);
            fprintf(pFile, "%04d-%02d-%02d %02d:%02d:%02d ：", tm_log->tm_year + 1900, tm_log->tm_mon + 1, tm_log->tm_mday, tm_log->tm_hour, tm_log->tm_min, tm_log->tm_sec);

            done = vfprintf(pFile, format, arg);
            va_end(arg);

            fflush(pFile);
            fclose(pFile);
            pFile = NULL;
            return done;
        }
    }
    return 0;
}

static int try_to_bind()
{
    listen_addr.sin_family = PF_INET;
    listen_addr.sin_addr.s_addr = inet_addr(ip);
    listen_addr.sin_port = htons(port);
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    if (-1 == bind(listen_fd, (struct sockaddr *)&listen_addr, sizeof(struct sockaddr_in)))
    {
        fprintf(stderr, "failed to bind %s:%d\n", ip, port);
        write_log("error_log.txt", "failed to bind %s:%d\r\n", ip, port);
        return -1;
    }

    listen(listen_fd, MAX_PROCESSES);
    return 0;
}

static DWORD WINAPI spawn_process(LPVOID lpParameter)
{
    int idx = process_idx++, ret;
    while (1)
    {

        STARTUPINFO si = {0}; //
        PROCESS_INFORMATION pi = {0};
        ZeroMemory(&si, sizeof(STARTUPINFO));
        si.cb = sizeof(STARTUPINFO);
        si.dwFlags = STARTF_USESTDHANDLES;
        si.hStdInput = (HANDLE)listen_fd;
        si.hStdOutput = INVALID_HANDLE_VALUE;
        si.hStdError = INVALID_HANDLE_VALUE;
        ret = CreateProcess(
            NULL,             // lpApplicationName
            path,             // lpCommandLine: php-cgi.exe
            NULL,             // lpProcessAttributes
            NULL,             // lpThreadAttributes
            TRUE,             // bInheritHandles
            CREATE_NO_WINDOW | CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB,
            NULL,            // lpEnvironment
            NULL,            // lpCurrentDirectory
            &si,
            &pi);

        if (0 == ret)
        {
            fprintf(stderr, "failed to create process %s, ret=%d\n", path, ret);
            write_log("error_log.txt", "failed to create process %s, ret=%d\r\n", path, ret);
            return -1;
        }
        else
        {
            fprintf(stdout, "successful to create process %s, ret=%d\n", path, ret);
            write_log("log.txt", "successful to create process %s, ret=%d\r\n", path, ret);
        }

        /* Use Job Control System */
        if (!AssignProcessToJobObject(FcpJobObject, pi.hProcess))
        {
            TerminateProcess(pi.hProcess, 1);
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
            write_log("error_log.txt", "AssignProcessToJobObject：failed\r\n");
            return -1;
        }

        if (!ResumeThread(pi.hThread))
        {
            TerminateProcess(pi.hProcess, 1);
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
            write_log("error_log.txt", "ResumeThread：failed\r\n");
            return -1;
        }

        process_fp[idx] = (int)pi.hProcess;
        WaitForSingleObject(pi.hProcess, INFINITE);
        process_fp[idx] = 0;
        CloseHandle(pi.hThread);
        write_log("log.txt", "WaitForSingleObject：Ready to create a new process\r\n");
    }

    return 0;
}

static int start_processes()
{
    int i;

    for (i = 0; i < number; i++)
    {
        threads[i] = CreateThread(
            NULL,            // Thread attributes
            64 * 1024,       // Stack size (0 = use default)
            spawn_process,   // Thread start address
            NULL,            // Parameter to pass to the thread
            0,               // Creation flags
            NULL);           // Thread id

        if (threads[i] == NULL)
        {
            fprintf(stderr, "failed to create thread %d\n", i);
            write_log("error_log.txt", "failed to create thread %d\r\n", i);
        }
    }

    WaitForMultipleObjects( 
        number,      // number of objects in array
        threads,     // array of objects
        TRUE,        // wait for all object
        INFINITE);   // infinite wait

    for (i = 0; i < number; i++)
    {
        CloseHandle(threads[i]);
    }
    return 0;
}

void init_win32()
{
    /* init win32 socket */
    static WSADATA wsa_data;
    if (WSAStartup((WORD)(1 << 8 | 1), &wsa_data) != 0)
    {
        exit(1);
    }    
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit;
    FcpJobObject = (HANDLE)CreateJobObject(NULL, NULL);
    if (FcpJobObject == NULL)
    {
        exit(1);
    }

    /* let all processes assigned to this job object
     * being killed when the job object closed
     */
    if (!QueryInformationJobObject(FcpJobObject, JobObjectExtendedLimitInformation, &limit, sizeof(limit), NULL))
    {
        CloseHandle(FcpJobObject);
        exit(1);
    }

    limit.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

    if (!SetInformationJobObject(FcpJobObject, JobObjectExtendedLimitInformation, &limit, sizeof(limit)))
    {
        CloseHandle(FcpJobObject);
        exit(1);
    }
}

/*
 * Usage: xxfpm php-cgi.exe -n 5 -p 8081
 */
int main(int argc, char **argv)
{
    prog_name = strrchr(argv[0], '/');

    if (prog_name == NULL)
        prog_name = strrchr(argv[0], '\\');
    if (prog_name == NULL)
        prog_name = argv[0];
    else
        prog_name++;

    if (argc == 1)
        usage(stderr);

    path = argv[1];

    opterr = 0;

    for (;;)
    {
        int ch;
        if ((ch = getopt_long(argc, argv, opts, longopts, NULL)) == EOF)
        {
            break;
        }    
        char *av = argv[optind];
        switch (ch)
        {
        case 'h':
            usage(stdout);
            break;
        case 'v':
            print_version();
            break;
        case 'n':
            number = atoi(av);
            if (number > MAX_PROCESSES)
            {
                fprintf(stderr, "exceeds MAX_PROCESSES!\n");
                write_log("error_log.txt", "exceeds MAX_PROCESSES!\r\n");
                number = MAX_PROCESSES;
            }
            break;
        case 'i':
            ip = av;
            break;
        case 'p':
            port = atoi(av);
            break;
        default:
            usage(stderr);
            break;
        }
    }

    init_win32();

    printf("Listening on %s:%s\n", ip, port);

    int ret;
    ret = try_to_bind();
    if (ret != 0)
    {
        return ret;
    }
    ret = start_processes();
    if (ret != 0)
    {
        return ret;
    }

    CloseHandle(FcpJobObject);
    WSACleanup();

    return 0;
}